Skip to content

feat(platform-objects,mcp): a member's agent finds a colleague through the read-only sys_user - #33180

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-83mt0o
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-83mt0o

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

epic:#32897#1 · os-tesla · session_01L9qibzL1RvtJP7jM7QZJaF

Fixes #32641

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:sys_user 声明了 enable.aiReadable: true。成员的 MCP 代理现在能用现有的 query_records / get_record 按邮箱或姓名找到同事、拿到用户 id,去填「负责人 / 审批人 / 员工」这类人员字段。没有新工具,任何写工具仍拒绝 sys_user。query_records 的说明和下发的 SKILL.md 教代理:按邮箱找唯一一个人;重名时用邮箱区分。
  • 为什么改:你裁了 B(6844187236):代理看到的和该成员在控制台人员选择器里看到的一样,同组织、只含通讯录字段;邮箱可见(6867150851)。
  • 风险与回滚:代理拿到的字段=控制台成员能读到的整行,减去 Admin 组:id、姓名、邮箱、邮箱已验证、两步验证开关、停用标记、头像、语言、上级、主业务单元、账号类型(人/AI)、责任人、来源、创建/更新时间。登录记录、锁定、封禁原因、登录手机号、AI 席位等 Admin 组字段不给,按它筛选也被拒;别的组织的用户查不到。这比 option B 原文「只有姓名/邮箱」宽,座位已就此问你(卡上 6867361507);若你要只留 id/姓名/邮箱,需要新加一条只针对代理的屏蔽规则,那是另一个决定。回滚即撤本 PR。
  • 先看:① packages/platform-objects/src/identity/sys-user.object.ts 的 aiReadable: true 与注释;② packages/qa/dogfood/test/colleague-lookup-over-mcp.dogfood.test.ts(端到端:找人、跨组织不可见、Admin 组被屏蔽、写被拒、填人员字段);③ packages/mcp/src/skill-md.ts 新加的「People are sys_user records」一条。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run test/colleague-lookup-over-mcp.dogfood.test.ts,预期 10 条全过。
  • 需要你做的:回答座位的字段集问题(控制台同款 / 只留 id、姓名、邮箱);在你回答前本 PR 不落地。

What changes

  • packages/platform-objects/src/identity/sys-user.object.ts: enable.aiReadable: true (the mechanism PR feat(approvals,mcp): an approver's agent lists the approvals waiting on them over MCP #33117 landed, 3eac63a9). Both MCP bridges already attest it through isAiReadExposed, so the five read tools (list_objects, describe_object, query_records, get_record, aggregate_records) admit sys_user; the write tools keep the plain sys_* guard. No code change in the admission.
  • No permission change. member_default already grants sys_user allowRead (default-permission-sets.ts, the sys_user object entry), scopes rows with sys_user_self + sys_user_org_members (id in current_user.org_user_ids), and withholds the Admin field group with identityAdminFieldSecurity('withhold'). Those are exactly the gates the console person picker (UserField → PeoplePicker → useRecordQuery, no field projection) is served through, so the agent's read and the picker's read are the same read.
  • What an agent is told. query_records' description: "To fill a person field, find the colleague's id: objectName "sys_user", where {"email":"..."} for exactly one person, or {"name":{"$contains":"..."}}; names repeat, so tell people who share a name apart by email." The served SKILL.md (packages/mcp/src/skill-md.ts) gains a Conventions bullet saying the same, plus "ask your human which one if unsure", and its list_objects line no longer says every system object is hidden. The Console "Download skill" copy (ui/apps/console/src/pages/developer/objectstack-skill.ts) carries the same two edits, because skill-md.test.ts holds it byte-identical to the served render.
  • Docs. content/docs/ai/connect-mcp.mdx (the default-exposure bullet names sys_user, what is served, that names repeat) and packages/mcp/README.md (the list_objects line names both declared objects).
  • Changeset .changeset/32641-sys-user-ai-readable.md: @objectstack/platform-objects, @objectstack/mcp, @object-ui/console patch, both declaration lines, the served field list.

The served field set (measured, live row)

For a plain member, on a colleague's row, over MCP and at the HTTP data door alike: accountable_user_id, banned, created_at, email, email_verified, id, image, locale, manager_id, name, primary_business_unit_id, principal_kind, source, two_factor_enabled, updated_at. That is every declared field outside the Admin group; the dogfood pins it as "declared minus Admin group", read off the declaration, so a field moved into or out of the group moves the pin with it.

Tests

  • packages/qa/dogfood/test/colleague-lookup-over-mcp.dogfood.test.ts (new, 10 tests). Real SecurityPlugin, real MCPServerPlugin, the live /mcp transport; no permission set in the file names sys_user (members hold the shipped baseline and an app set naming the app's one object). Fixture: a member, two colleagues who share one name, and a third user of that name moved into another organization (membership and sessions); synthetic Admin group values on one colleague. Armed controls: the group is non-empty and covers the fixture fields; the values are at rest; the outsider is in another organization and the name matches three rows at rest.
    • by name, the member finds exactly the two colleagues and tells them apart by email; $contains on a partial name, the same two; by email, exactly one id;
    • the outsider is invisible: by name, by email (empty), in the unfiltered list, by id (get_record refused, no email leaked), by count (n: 2);
    • the Admin group: not on the served row, not on a projection naming it, and a filter on failed_login_count is refused; control: the platform admin's get_record is served last_login_ip;
    • the agent's query_records and the member's GET /data/sys_user return the same ids and the same field set, which equals the declared non-Admin fields;
    • create_record, update_record (own row and a colleague's), delete_record, delete_records are each refused with the system-object sentence, and nothing changed at rest;
    • the query_records text names sys_user and email;
    • end to end: find by email, create_record on the app object with assignee = the found id, the stored row holds it.
  • packages/platform-objects/src/identity/sys-user-ai-read.test.ts (new, 2): the declaration and isAiReadExposed; apiMethods unchanged.
  • packages/mcp/src/skill-md.test.ts (+1 describe): the served skill's bullet names query_records, "email", $contains, "tell them apart by email", "read only"; the old "are hidden" claim is gone. The existing byte-identity pin holds the Console copy.
  • packages/qa/dogfood/test/approval-read-over-mcp.dogfood.test.ts: its list_objects pin said sys_approval_request is the only listed sys_* object; it now lists sys_approval_request and sys_user.

Ablations

Each on a committed head with scripts/ablation-replace.mjs --hold, the package rebuilt, scripts/ablation-dist-preflight.mjs proving the mutation in dist/ before the run and gone after the restore, restore proved blob == HEAD and git status --porcelain empty.

Ablation Result
A1: the aiReadable: true line deleted (platform-objects rebuilt). This is also the measurement of main declaration pin red (1 failed, 1 passed); dogfood 8 failed, 2 passed, every read answering Object "sys_user" is a system object and is not exposed via MCP (the two still green: the write refusals and the tool text)
A2: sys_user_org_members widened to true (plugin-security rebuilt; the anchor occurs three times, member, viewer and external sets, all mutated) dogfood 2 failed, 8 passed: the name lookup returns three ids, the outsider appears
A3: member_default keeps the Admin group (identityAdminFieldSecurity keep, plugin-security rebuilt) dogfood 2 failed, 8 passed: the withheld-group pin (13 group fields served) and the console-set pin
A4: update_record uses the read guard (mcp rebuilt) dogfood 1 failed, 9 passed: update_record on sys_user answered a record

A4's first try used a comment as the marker, which the bundler strips, so the preflight refused it and nothing ran; the second used an identifier. A2's first try used an anchor that occurs three times without --expect 3, so nothing was written and the run was void; it was rerun with --expect 3.

Acceptance notes

Verification

Local, on head 1e1c1561:

  • pnpm --filter @objectstack/platform-objects test 122 files / 1957 tests passed; pnpm --filter @objectstack/mcp test (after merging main) 64 / 1141 passed.
  • pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/colleague-lookup-over-mcp.dogfood.test.ts test/approval-read-over-mcp.dogfood.test.ts 2 files / 20 tests passed (after merging main).
  • typecheck: platform-objects 0, mcp 0, dogfood 0 (the dogfood one at an 8 GiB heap: the default 4 GiB OOMs, exit 134).
  • Derived gates (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, 100 commands, each with stdin closed) and pnpm lint: all exit 0. Three exited 3 (PREREQUISITE NOT MET, nothing measured) on the first pass and exit 0 on a rerun after the precondition was met: check:dual-build-cjs-loads and check:skill-examples (packages without dist/, built), check-plugin-teardown-shape --self-test (its pinned fixture commit, fetched into this shallow clone). --ran reconciliation: 100 derived, 100 run, 0 NOT-MEASURED (a derived zero: every line carries its exit code); pnpm lint recorded outside the derivation.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…h the read-only sys_user

sys_user declares enable.aiReadable, so the MCP read tools admit it under
the gates a console read already runs: the caller's own row and their
organization's members, the Admin field group withheld. No write tool
opens it. The query_records text and the served SKILL.md say how to find
a colleague for a person field and to tell namesakes apart by email.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzL1RvtJP7jM7QZJaF
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/mcp, @objectstack/platform-objects, touching 2 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/mcp/README.md, packages/mcp/src/skill-md.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/connect-mcp.mdx (via pending_approvers (literal, a string literal in registerObjectTools))
  • content/docs/automation/approvals.mdx (via pending_approvers (literal, a string literal in registerObjectTools))
  • content/docs/automation/flows.mdx (via pending_approvers (literal, a string literal in registerObjectTools))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/mcp/README.md, packages/mcp/src/skill-md.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: sys_user (literal, 53 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9e235344f19c483b18c54fb063143dbef0de837b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from dba2fbe992679f06c05a0064cd2a5430aa811c3c — the merge of head 1e1c156181938f70d797754ed6b940b6f7873574 into base 9e235344f19c483b18c54fb063143dbef0de837b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dba2fbe992679f06c05a0064cd2a5430aa811c3c && git checkout dba2fbe992679f06c05a0064cd2a5430aa811c3c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9e235344f19c483b18c54fb063143dbef0de837b 1e1c156181938f70d797754ed6b940b6f7873574 && git checkout -B drift-repro 9e235344f19c483b18c54fb063143dbef0de837b && git merge --no-ff 1e1c156181938f70d797754ed6b940b6f7873574

node scripts/docs-audit/affected-docs.mjs --json 9e235344f19c483b18c54fb063143dbef0de837b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9e235344f19c483b18c54fb063143dbef0de837b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#32897#1 · os-tesla · session_01L9qibzoApSyVPuia3NCrBY

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 1e1c156181938f70d797754ed6b940b6f7873574
Local-runs: none

Owed by the Clause-②: yes (widening) declaration alone (no packages/spec/src/**, no governed path); the lane seat's review of record. Inputs: card #32641 and every comment (rulings 6844187236 B, 6867150851 email visible, 6868810528 field set = the console person picker's set), the PR body and file list, the net diff, the check-runs on this head.

① Derived judgments

  • Widening, declared right: the MCP read tools (list_objects, describe_object, query_records, get_record, aggregate_records) admit sys_user, which they refused before with the system-object sentence. The diff's one behaviour line is aiReadable: true in sys_user's enable (packages/platform-objects/src/identity/sys-user.object.ts); admission goes through the existing isAiReadExposed path from PR feat(approvals,mcp): an approver's agent lists the approvals waiting on them over MCP #33117, unchanged.
  • Rows and fields = the ruled set: no new mask or permission line; the read runs under member_default's sys_user_self + sys_user_org_members row scopes and identityAdminFieldSecurity('withhold') (plugin-security: an org member reading a colleague's sys_user row is served the Admin-group fields (last_login_ip/at, failed_login_count, locked_until, ban_*, phone_number) — and through sys_activity, their sign-in history #21237). That is exactly ruling 6868810528 (「和界面选人一样」), with email visible (6867150851). The PR body and changeset list the 15 fields a plain member is served.
  • No write opens: create_record / update_record / delete_record / delete_records keep refusing every sys_* object; pinned by the dogfood suite (five write tools refused).
  • Agent guidance: query_records' description and the served SKILL.md (packages/mcp/src/skill-md.ts, with its byte-identical Console copy) say how to find a colleague's id, and to tell namesakes apart by email (6867150851). True of the code.
  • Outsider invisibility (by name, email, id, count), Admin-group withholding on results and refusal on filters, and agent == GET /api/v1/data/sys_user ids and fields are pinned in colleague-lookup-over-mcp.dogfood.test.ts; ablations ran red (report 6868313418).

② Semver level

Clause-②: yes (widening), Metadata-protocol: none (no authorable key changes; sys_user is a platform object). Changeset: @objectstack/platform-objects, @objectstack/mcp, @object-ui/console patch. Patch is right for an additive read admission under existing gates; the changeset names the widening in its own Clause-②: line.

③ Boundary flags

  • Dev deviations (Console skill mirror, approval-read dogfood pin): accepted, claim amended 6868850311.
  • The field-set fork the dev raised: ruled 6868810528; the PR is that shape.
  • Check-runs on this head: 55 — no failure (one docs-flag lane still running at write time).

Implemented-by: claude/project-thread-83mt0o
Reviewed-by: session_01L9qibzoApSyVPuia3NCrBY

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#32897#1 · os-tesla · session_01L9qibzoApSyVPuia3NCrBY

维护者速读 · PR #33180 @ 1e1c1561 · 2026-11-14T01:28Z

  • 改了什么:sys_user 声明 enable.aiReadable: true。成员的 MCP 代理可以用现有的 query_records / get_record 按邮箱或姓名找到同事、拿到 id,去填人员字段。没有新工具,写工具仍拒绝 sys_user。
  • 为什么改:mcp: an agent finds a colleague through a read-only sys_user (id, name, email) within the caller's organization visibility — ruled B, email visible #32641,裁决 B(6844187236);字段集你已裁为「和界面选人一样」(6868810528)。
  • 权限与边界:代理读到的=成员在控制台选人时读到的:本组织成员、Admin 组以外的字段;别的组织查不到,按 Admin 组字段筛选被拒。没有改任何权限集。
  • 风险与回滚:patch 级。回滚就是 revert,或删掉那一行声明。
  • 先看:packages/platform-objects/src/identity/sys-user.object.ts 的那一行;packages/qa/dogfood/test/colleague-lookup-over-mcp.dogfood.test.ts。
  • 需要你做的:无,席位落地。正文「在你回答前本 PR 不落地」那句已由 6868810528 了结。
  • 席位意见:落地。

席位核对

  • 形态: draft → main。正文 Fixes #32641,合并后该卡应当关;正文没有挨着别的 open 卡号的闭合关键词。Clause-②: yes (widening)、Metadata-protocol: none。一个 changeset(@objectstack/platform-objects、@objectstack/mcp、@object-ui/console patch)。没有改 content/docs/releases/、skills/**。
  • 前提实测: A1(删掉声明行,即 main 的状态)下 dogfood 8 红、声明 pin 红;head 上 10/10 绿。A2–A4 各自转红后已恢复(正文消融表)。
  • 门禁 check-runs @ 1e1c1561: 共 55 个,45 success、9 skipped、1 in_progress。那一个是 Flag docs affected by code changes(docs-drift-check.yml):工作流自述只做提示、不判失败,timeout-minutes: 15,00:16Z 起跑到现在已超时一小时仍挂着,是一条没收尾的幽灵任务,不是必需检查。其余全绿。本地派生门禁 100/100 exit 0,pnpm lint 0。
  • 契约复审: 席位记录 6869121715 PASS(TIER_DEFAULT,declaration face)。
  • 接受的偏差: 认领文件面以外改了两处,均为连带:Console 下载版 objectstack-skill.ts(被 skill-md.test.ts 钉成与下发版逐字节相同)和 approval-read-over-mcp.dogfood.test.ts 的 list_objects pin;认领已补记 6868850311。
  • 范围外去向: stdio 主机下 sys_user 的行与字段门由 mcp: stdio describe_object and the objects resource serve readonly: false for a stored expression field whose expression the caller's field mask hides, while the HTTP door answers true #33156 / MCP stdio get_record answers a missing or hidden id with uncoded error text, while the HTTP door answers RECORD_NOT_FOUND 404 #33030 那一卡负责(PR fix(mcp): the stdio door answers get_record misses and describe_object readonly as the HTTP door does #33175 已合);本 PR 不钉。
  • 落地条款:
    • 维护者常设落地令「按 pm-dispatch 审过的pr你可以自主落地」(2026-10-30T14:20Z)。
    • 非受管面(check-governed-merges --pr 33180:0/11 命中);459 行 ≤ 3000。
    • K = 9(PRBASE 9e235344 取自 Test Core 计划的 Affected-set diff base;origin/main f45c0125),小于 15,不需要先 merge main。
    • 没有新的语种包。

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 01:29
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 6a36b79 Nov 14, 2026
55 of 56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-83mt0o branch November 14, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp: an agent finds a colleague through a read-only sys_user (id, name, email) within the caller's organization visibility — ruled B, email visible

2 participants